import sys
import traceback
from typing import Callable, Literal, TypeVar, Union

T = TypeVar('T')
NoDefault = object()


def _suffix_prompt(prompt: str) -> tuple[str, str]:
    if prompt.endswith(":"):
        return prompt[:-1], ":"
    if prompt.endswith("?"):
        return prompt[:-1], "?"
    if prompt.endswith("："):
        return prompt[:-1], "："
    if prompt.endswith("？"):
        return prompt[:-1], "？"
    return prompt, ":"


def _construct_prompt(
    prompt: str, *,
    default: T = NoDefault,
    hint: Callable[[T], str] | str,
) -> str:
    prompt, suffix = _suffix_prompt(prompt)
    if isinstance(hint, Callable):
        hint = hint(default)
    elif isinstance(hint, str):
        pass
    else:
        raise TypeError(f"Unexpected type of hint: {type(hint)}")
    if hint:
        hint = f" ({hint})"
    return f"{prompt}{suffix}{hint} "


def _read_result(
    prompt: str,
    default: T = NoDefault, *,
    parse_func: Callable[[str], T],
    error_func: Callable[[str, Exception], None] | Literal['raise'],
):
    while True:
        text = input(prompt)
        try:
            if text:
                return parse_func(text)
            if default is not NoDefault:
                return default
            raise ValueError("Unexpected empty input!")
        except Exception as err:
            if error_func == 'raise':
                raise
            elif callable(error_func):
                error_func(text, err)
            else:
                raise TypeError(f"Unexpected type of error_func: {type(error_func)}")


def _default_hint_func(default: T) -> str:
    if default is NoDefault:
        return ""
    return f"default={default}"


def _default_error_func(text: str, err: Exception | None):
    print(f"Illegal input: {text}")
    if err is not None:
        traceback.print_exception(err, file=sys.stderr)


def input_generic(
    prompt: str, *,
    default: T = NoDefault,
    hint: Callable[[str], str] | str = _default_hint_func,
    parse_func: Callable[[str], T] = str,
    error_func: Callable[[str, Exception], None] | Literal['raise'] = _default_error_func,
) -> T:
    """ Asks the user to input something of some type """
    prompt = _construct_prompt(prompt, default=default, hint=hint)
    return _read_result(prompt, default, parse_func=parse_func, error_func=error_func)


# typed input

def input_text(
    prompt: str, *,
    default: Union[str, T] = NoDefault,
) -> Union[str, T]:
    """ Asks the user to input text """
    return input_generic(prompt, default=default, parse_func=str)


def input_int(
    prompt: str, *,
    default: Union[int, T] = NoDefault,
) -> Union[int, T]:
    """ Asks the user to input an integer """
    return input_generic(prompt, default=default, parse_func=int)


def input_float(
    prompt: str, *,
    default: Union[float, T] = NoDefault,
) -> Union[float, T]:
    """ Asks the user to input a float number """
    return input_generic(prompt, default=default, parse_func=float)


def input_yes_or_no(
    prompt: str, *,
    default: Union[bool, T] = NoDefault,
) -> Union[bool, T]:
    """ Asks the user to input yes or no. """
    if default is NoDefault:
        hint = "y/n"
    elif default is True:
        hint = "Y/n"
    elif default is False:
        hint = "y/N"
    else:
        raise TypeError(f"Unexpected type of default: {type(default)}")

    def parse_func(text: str) -> bool:
        text_lower = text.lower()
        if text_lower in ('y', 'yes'):
            return True
        elif text_lower in ('n', 'no'):
            return False
        else:
            raise ValueError(f"Expected 'y' or 'n', got {text}")

    return input_generic(prompt, default=default, hint=hint, parse_func=parse_func)
